import xml.etree.ElementTree as ET
from tempfile import SpooledTemporaryFile
from email.utils import formatdate
from pathlib import PurePath
from typing import Literal, Set, List, Union, Optional
from urllib.parse import urlparse, unquote

from fastapi import HTTPException, Response, Request, UploadFile
from fastapi.responses import StreamingResponse
from starlette._compat import md5_hexdigest
from starlette.background import BackgroundTask
from starlette.responses import guess_type

from my_webdav.resource_model.base_model import DavCollection, DavFileMember, ResourceCreator, BaseResource
from ..client_115.schemas import FileResp, FileOrFolder
from ..client_115.digest import digest
from .data_operate import OperateLayer

ResourceMode = Literal[1, 2]
BASIC_PROPS: Set[str] = {'displayname', 'getcontenttype', 'getcontentlength', 'iscollection'}
ALL_PROPS: Set[str] = {
    'displayname', 'getcontenttype', 'getcontentlength', 'iscollection', 'ishidden',
    'resourcetype'
}
Pan115Model = Union['Pan115Collection', 'Pan115File']

MAX_SIZE_MEM: int = 1024 * 1024  # SpooledTemporaryFile的max_size


def info_to_model(
        info: FileOrFolder, op_layer: OperateLayer, path: PurePath = None, dir_path: PurePath = None
) -> Union['Pan115Collection', 'Pan115File']:
    tp = Pan115File if isinstance(info, FileResp) else Pan115Collection
    if path:
        model = tp(path, info, op_layer)
    elif dir_path:
        full_path = dir_path.joinpath(info.n)
        model = tp(full_path, info, op_layer)
    else:
        raise ValueError

    return model


def get_href(path: PurePath, flg: ResourceMode):
    tail = '/' if flg == 1 else ''
    href = path.as_posix()
    if href != "/":
        href = f"{href}{tail}"
    return href


class Pan115Member(object):
    FLG: ResourceMode

    def __init__(
            self, path: PurePath, info: FileOrFolder, op_layer: OperateLayer
    ):
        self.path = path
        self.info = info
        self.op_layer = op_layer
        self.href = get_href(path, self.FLG)
        self.displayname = path.name if path != PurePath("/") else "/"
        self.children: List[Union['Pan115Collection', 'Pan115File']] = []

        self.PROP_MAPPING = {
            "getlastmodified": formatdate(self.info.te, usegmt=True) if self.info.te is not None else '',
            "displayname": self.displayname,
            "getetag": md5_hexdigest(
                f"{self.info.te}-{self.info.s}".encode(), usedforsecurity=False
            ) if self.FLG == 2 else '',
            "getcontenttype": "httpd/unix-directory" if self.FLG == 1 else guess_type(self.displayname)[0],
            "iscollection": "1" if self.FLG == 1 else "",
            "getcontentlength": "" if self.FLG == 1 else str(self.info.s),
            "href": self.href
        }
        self.size = None if self.FLG == 1 else self.info.s

    def get_prop(self, prop: str) -> ET.Element:
        elem = ET.Element(f"D:{prop}")

        prop_val = self.PROP_MAPPING.get(prop.lower())
        if prop_val:
            elem.text = prop_val
            return elem

        if prop.lower() == "resourcetype":
            if self.FLG == 1:
                elem.append(ET.Element("D:collection"))
        return elem

    def compose_resp_xml(self, props: List[str], only_propname: bool = False) -> ET.Element:
        """
        组装单个资源响应的xml
        :param only_propname: 只返回属性名
        :param props:
        :return:
        """
        resp_root = ET.Element("D:response")

        href_elem = self.get_prop("href")
        prop_stat_elem = ET.Element("D:propstat")
        prop_elem = ET.Element("D:prop")

        for prop in props:
            if only_propname:
                elem = ET.Element(f"D:{prop}")
            else:
                elem = self.get_prop(prop)
            prop_elem.append(elem)

        status_elem = ET.Element("D:status")
        status_elem.text = "HTTP/1.1 200 OK"

        prop_stat_elem.append(prop_elem)
        prop_stat_elem.append(status_elem)

        resp_root.append(href_elem)
        resp_root.append(prop_stat_elem)

        return resp_root

    def prop_xml(
            self, depth: str, props: List[str], only_propname: bool = False,
            all_prop: bool = False
    ) -> bytes:
        if only_propname or all_prop:
            props = list(ALL_PROPS)

        multi_status_elem = ET.Element("D:multistatus", {"xmlns:D": "DAV:"})
        resp = self.compose_resp_xml(props, only_propname)
        multi_status_elem.append(resp)

        if depth != "0" and self.FLG == 1:
            # 查询子成员
            for res_model in self.children:
                resp = res_model.compose_resp_xml(props, only_propname)
                multi_status_elem.append(resp)

        return ET.tostring(multi_status_elem, xml_declaration=True, encoding='utf-8')

    def add_children(self, children):
        raise NotImplementedError

    async def delete(self):
        if self.FLG == 1:
            res_id, dir_id = self.info.cid, self.info.pid
            is_dir = True
        else:
            res_id, dir_id = self.info.fid, self.info.cid
            is_dir = False
        try:
            await self.op_layer.delete_res(res_id, dir_id, self.path, is_dir)
        except Exception:
            raise HTTPException(400)

    async def move(self, dst: Optional[str], overwrite: Literal["F", "T"]):
        _ = overwrite
        dst_path = PurePath(urlparse(unquote(dst)).path)
        res_id = self.info.fid if self.FLG == 2 else self.info.cid
        await self.op_layer.move_resource(
            dst_path, str(res_id), self.path, True if self.FLG == 1 else False
        )

    async def copy(
            self, dst: Optional[str], overwrite: Literal["F", "T"], depth: Literal["0", "infinity"]
    ):
        """
        复制资源到dst处; 以115网盘操作逻辑为准, 因此overwrite和depth参数无效
        :param dst:
        :param overwrite:
        :param depth:
        :return:
        """
        _, _ = overwrite, depth
        dst_path = PurePath(urlparse(unquote(dst)).path)
        res_id = self.info.fid if self.FLG == 2 else self.info.cid
        await self.op_layer.copy_resource(dst_path, str(res_id))


# 多重继承注意MRO
class Pan115Collection(DavCollection, Pan115Member):
    def add_children(self, children: List[Union['Pan115Collection', 'Pan115File']]):
        self.children.extend(children)

    async def delete(self):
        await super(BaseResource, self).delete()

    async def move(self, dst: Optional[str], overwrite: Literal["F", "T"]):
        await super(BaseResource, self).move(dst, overwrite)

    async def copy(
            self, dst: Optional[str], overwrite: Literal["F", "T"], depth: Literal["0", "infinity"]
    ):
        await super(BaseResource, self).copy(dst, overwrite, depth)


class Pan115File(DavFileMember, Pan115Member):
    def add_children(self, children):
        raise NotImplementedError

    async def response_for_get(
            self, req_rng: str = None, chunk_size: int = 64 * 1024,
            head_only: bool = False
    ):
        pickcode = self.info.pc
        client = self.op_layer.client
        download_resp = await client.get_download_resp(pickcode, req_rng, sync=True, head_only=head_only)

        headers = {k: v for k, v in download_resp.headers.items()}
        if head_only:
            return Response(status_code=206 if req_rng else 200, headers=headers)
        resp = StreamingResponse(
            content=download_resp.iter_bytes(chunk_size),
            status_code=206 if req_rng else 200,
            headers=headers,
            background=BackgroundTask(download_resp.close)
        )
        return resp

    async def delete(self):
        await super(BaseResource, self).delete()

    async def move(self, dst: Optional[str], overwrite: Literal["F", "T"]):
        await super(BaseResource, self).move(dst, overwrite)

    async def copy(
            self, dst: Optional[str], overwrite: Literal["F", "T"], depth: Literal["0", "infinity"]
    ):
        await super(BaseResource, self).copy(dst, overwrite, depth)


class Pan115ResourceCreator(ResourceCreator):
    def __init__(self, dst: PurePath, op_layer: OperateLayer):
        super().__init__(dst)
        self.op_layer = op_layer

    async def make_collection(self):
        await self.op_layer.make_collection(self.dst)

    async def put_file(self, request: Request):
        with SpooledTemporaryFile(max_size=MAX_SIZE_MEM) as ft:
            # noinspection PyTypeChecker
            upload_file = UploadFile(ft, size=0)
            async for chunk in request.stream():
                await upload_file.write(chunk)

            await upload_file.seek(0)
            digest_res = await digest(upload_file)
        return digest_res
